/*
 * SDIR.C
 *
 * Colorful, sorted and optionally rich directory enumeration
 * for Windows.
 *
 * This module implements the core logic of displaying directories.
 *
 * Copyright (c) 2014-15 Malcolm J. Smith
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

#include "sdir.h"


DWORD SdirAllocatedDirents     = 1000;
PSDIR_DIRENT SdirDirCollection = NULL;
PSDIR_DIRENT * SdirDirSorted   = NULL;
ULONG SdirDirCollectionCurrent = 0;
ULONG SdirDirCollectionLongest = 0;
ULONG SdirDirCollectionTotalNameLength = 0;

PSDIR_OPTS Opts = NULL;
PSDIR_SUMMARY Summary = NULL;


BOOL
SdirDisplayCollection();

BOOL
SdirCaptureFoundItemIntoDirent (
    __out PSDIR_DIRENT CurrentEntry,
    __in PWIN32_FIND_DATA FindData,
    __in LPTSTR FullPath
    ) 
{
    DWORD i;

    //
    //  Copy over the data from Win32's FindFirstFile into our own structure.
    //

    for (i = 0; i < SdirGetNumSdirOptions(); i++) {

        PSDIR_FEATURE Feature;
        Feature = SdirFeatureByOptionNumber(i);

        //
        //  If we're displaying or sorting, we need the data to use
        //

        if ((Feature->Flags & SDIR_FEATURE_COLLECT) &&
               SdirOptions[i].CollectFn) {

            SdirOptions[i].CollectFn(CurrentEntry, FindData, FullPath);
        }
    }

    //
    //  Determine the color to display each entry from extensions and attributes.
    //  If we're asked to hide, just walk away from the entry we created and
    //  continue.
    //
    
    if (!SdirApplyAttribute(CurrentEntry, &CurrentEntry->RenderAttributes)) {
        CurrentEntry->RenderAttributes = SDIR_DEFAULT_COLOR;
    }

    return TRUE;
}

BOOL
SdirUpdateFindDataFromFileInformation (
    __out PWIN32_FIND_DATA FindData,
    __in LPTSTR FullPath
    )
{
    HANDLE hFile;
    BY_HANDLE_FILE_INFORMATION FileInfo;

    hFile = CreateFile(FullPath,
                       FILE_READ_ATTRIBUTES,
                       FILE_SHARE_READ|FILE_SHARE_WRITE|FILE_SHARE_DELETE,
                       NULL,
                       OPEN_EXISTING,
                       FILE_FLAG_BACKUP_SEMANTICS|FILE_FLAG_OPEN_REPARSE_POINT|FILE_FLAG_OPEN_NO_RECALL,
                       NULL);

    if (hFile != INVALID_HANDLE_VALUE) {

        GetFileInformationByHandle(hFile, &FileInfo);

        FindData->dwFileAttributes = FileInfo.dwFileAttributes;
        FindData->ftCreationTime = FileInfo.ftCreationTime;
        FindData->ftLastAccessTime = FileInfo.ftLastAccessTime;
        FindData->ftLastWriteTime = FileInfo.ftLastWriteTime;
        FindData->nFileSizeHigh = FileInfo.nFileSizeHigh;
        FindData->nFileSizeLow  = FileInfo.nFileSizeLow;

        CloseHandle(hFile);
        return TRUE;
    }
    return FALSE;
}

WORD
SdirRenderAttributesFromPath (
    __in LPTSTR FullPath
    )
{
    HANDLE hFind;
    WIN32_FIND_DATA FindData;
    SDIR_DIRENT CurrentEntry;

    memset(&CurrentEntry, 0, sizeof(CurrentEntry));

    hFind = FindFirstFile(FullPath, &FindData);
    if (hFind != INVALID_HANDLE_VALUE) {
        SdirCaptureFoundItemIntoDirent(&CurrentEntry, &FindData, FullPath);
        FindClose(hFind);
        return CurrentEntry.RenderAttributes;
    } else {
        LPTSTR Dummy;
        DWORD DummyLen;

        DummyLen = (DWORD)_tcslen(FullPath) + 2;

        Dummy = HeapAlloc(GetProcessHeap(), 0, DummyLen * sizeof(TCHAR));
        if (Dummy == NULL) {
            return SDIR_DEFAULT_COLOR;
        }

        memset(&FindData, 0, sizeof(FindData));
        _stprintf_s(Dummy, DummyLen, _T("%s\\"), FullPath);
        SdirUpdateFindDataFromFileInformation(&FindData, Dummy);
        SdirCaptureFoundItemIntoDirent(&CurrentEntry, &FindData, Dummy);
        HeapFree(GetProcessHeap(), 0, Dummy);
        return CurrentEntry.RenderAttributes;
    }
}

BOOL
SdirAddToCollection (
    __in PWIN32_FIND_DATA FindData,
    __in LPTSTR FullPath
    ) 
{
    PSDIR_DIRENT CurrentEntry;
    DWORD i, j;
    DWORD CompareResult = 0;

    if (SdirDirCollectionCurrent >= SdirAllocatedDirents) {
        if (SdirDirCollectionCurrent < UINT_MAX) {
            SdirDirCollectionCurrent++;
        }
        return FALSE;
    }

    CurrentEntry = &SdirDirCollection[SdirDirCollectionCurrent];

    SdirDirCollectionCurrent++;

    SdirCaptureFoundItemIntoDirent(CurrentEntry, FindData, FullPath);

    if (Opts->BriefRecurseDepth == 0 &&
        CurrentEntry->RenderAttributes & SDIR_ATTRIBUTE_HIDE) {

        SdirDirCollectionCurrent--;
        return TRUE;
    }

    if (CurrentEntry->FileNameLengthInChars > SdirDirCollectionLongest) {
        SdirDirCollectionLongest = CurrentEntry->FileNameLengthInChars;
    }

    SdirDirCollectionTotalNameLength += CurrentEntry->FileNameLengthInChars;

    if (Opts->FtSummary.Flags & SDIR_FEATURE_COLLECT) {
        SdirCollectSummary(CurrentEntry);
    }

    //
    //  Now that our internal entry is fully poulated, insert it into the correct sorted position.
    //  As an optimization, check if we just need to insert at the end (for file name sort on
    //  NTFS, this is the common case.)
    //

    if (SdirDirCollectionCurrent > 1 &&
        Opts->Sort[0].CompareFn(SdirDirSorted[SdirDirCollectionCurrent - 2], CurrentEntry) == Opts->Sort[0].CompareInverseCondition) {

        SdirDirSorted[SdirDirCollectionCurrent - 1] = CurrentEntry;
        return TRUE;
    }

    //
    //  MSFIX This algorithm seems really stupid.  Since we know the sort
    //  list is sorted, shouldn't we binary search?
    //

    for (i = 0; i < SdirDirCollectionCurrent - 1; i++) {

        for (j = 0; j < Opts->CurrentSort; j++) {
            CompareResult = Opts->Sort[j].CompareFn(SdirDirSorted[i], CurrentEntry);

            if (CompareResult == Opts->Sort[j].CompareBreakCondition ||
                CompareResult == Opts->Sort[j].CompareInverseCondition) {
                break;
            }
        }

        if (CompareResult == Opts->Sort[j].CompareBreakCondition) {
            for (j = SdirDirCollectionCurrent - 1; j > i; j--) {
                SdirDirSorted[j] = SdirDirSorted[j - 1];
            }
            SdirDirSorted[i] = CurrentEntry;
            return TRUE;
        }
    }
    SdirDirSorted[SdirDirCollectionCurrent - 1] = CurrentEntry;
    return TRUE;
}

BOOL
SdirEnumeratePath (
    __in LPCTSTR FindStr
    )
{
    HANDLE hFind;
    WIN32_FIND_DATA FindData;
    LPTSTR FinalPart;
    DWORD SizeCopied;
    LPTSTR FullPath;
    DWORD FullPathLength;
    ULONG DirEntsToPreserve;
    PSDIR_DIRENT NewSdirDirCollection;
    PSDIR_DIRENT * NewSdirDirSorted;

    //
    //  At this point we should have a directory and an enumeration criteria.
    //  Copy the directory name and clobber the enumeration criteria.
    //
    
    if (Opts->ParentName != NULL) {
        HeapFree(GetProcessHeap(), 0, Opts->ParentName);
        Opts->ParentName = NULL;
    }

    if (!SdirGetFullPathNameReturnAllocation(FindStr, &Opts->ParentName, &FinalPart)) {
        SdirDisplayError(GetLastError(), _T("SdirGetFullPathName"));
        return FALSE;
    }

    if (FinalPart) {
        *FinalPart = '\0';
    }
    Opts->ParentNameLength = (DWORD)_tcslen(Opts->ParentName);

    FullPathLength = Opts->ParentNameLength + SDIR_MAX_FILE_AND_STREAM_NAME;
    FullPath = HeapAlloc(GetProcessHeap(), 0, FullPathLength * sizeof(TCHAR));
    if (FullPath == NULL) {
        SdirDisplayError(GetLastError(), _T("HeapAlloc"));
        return FALSE;
    }

    if (Opts->FtSummary.Flags & SDIR_FEATURE_COLLECT) {

        LARGE_INTEGER Junk;

        //
        //  If GetDiskFreeSpaceEx fails, fall back to GetDiskFreeSpace.  According
        //  to MSDN, this can sometimes be present and fail with not supported.
        //

        if (Summary->VolumeSize.QuadPart == 0 &&
            (Opts->GetDiskFreeSpaceEx == NULL ||
             !Opts->GetDiskFreeSpaceEx(Opts->ParentName, &Junk, &Summary->VolumeSize, &Summary->FreeSize))) {

            ULONG BytesPerSector;
            ULONG SectorsPerCluster;
            ULONG FreeClusters;
            ULONG TotalClusters;

            if (GetDiskFreeSpace(Opts->ParentName, &SectorsPerCluster, &BytesPerSector, &FreeClusters, &TotalClusters)) {

                SdirFileSizeFromLargeInt(&Summary->VolumeSize) = SectorsPerCluster * BytesPerSector * TotalClusters;
                SdirFileSizeFromLargeInt(&Summary->FreeSize) = SectorsPerCluster * BytesPerSector * FreeClusters;
            }
        }
    }

    //
    //  We loop enumerating all the files.  Hopefully for common directories
    //  we'll allocate a big enough buffer in the first case and we can then
    //  just populate that buffer and display it.  If the directory is large
    //  enough though, we count the number of entries, loop back, and allocate
    //  a large enough buffer to hold the result, then enumerate it again.
    //  Because we sort the output, we must keep the entire set in memory to
    //  be able to meaningfully process it.
    //

    DirEntsToPreserve = SdirDirCollectionCurrent;

    do {

        //
        //  If this is a subsequent pass and we didn't allocate a big enough
        //  buffer, reallocate our buffers.  Add an extra 100 just in case
        //  we're still adding files in real time.
        //

        if (SdirDirCollectionCurrent >= SdirAllocatedDirents || SdirDirCollection == NULL) {

            SdirAllocatedDirents = SdirDirCollectionCurrent;
            if (SdirAllocatedDirents < UINT_MAX - 100) {
                SdirAllocatedDirents += 100;
            }

            NewSdirDirCollection = HeapAlloc(GetProcessHeap(), 0, SdirAllocatedDirents * sizeof(SDIR_DIRENT));
            if (NewSdirDirCollection == NULL) {
                SdirAllocatedDirents = SdirDirCollectionCurrent;
                SdirDisplayError(GetLastError(), _T("HeapAlloc"));
                HeapFree(GetProcessHeap(), 0, FullPath);
                return FALSE;
            }
    
            NewSdirDirSorted = HeapAlloc(GetProcessHeap(), 0, SdirAllocatedDirents * sizeof(PSDIR_DIRENT));
            if (NewSdirDirSorted == NULL) {
                SdirAllocatedDirents = SdirDirCollectionCurrent;
                HeapFree(GetProcessHeap(), 0, NewSdirDirCollection);
                SdirDisplayError(GetLastError(), _T("HeapAlloc"));
                HeapFree(GetProcessHeap(), 0, FullPath);
                return FALSE;
            }

            //
            // Copy back any previous data.  This occurs when multiple criteria are specified, eg.,
            // "*.a *.b".  Apply fixups to everything in the sorted array which were based on the
            // previous collection and now need to be based on the new one.
            //
    
            if (DirEntsToPreserve > 0) {
                memcpy(NewSdirDirCollection, SdirDirCollection, sizeof(SDIR_DIRENT)*DirEntsToPreserve);
                for (SizeCopied = 0; SizeCopied < DirEntsToPreserve; SizeCopied++) {
                    NewSdirDirSorted[SizeCopied] = (PSDIR_DIRENT)((PUCHAR)SdirDirSorted[SizeCopied] -
                                                                  (PUCHAR)SdirDirCollection +
                                                                  (PUCHAR)NewSdirDirCollection);
                }
            }
    
            if (SdirDirCollection != NULL) {
                HeapFree(GetProcessHeap(), 0, SdirDirCollection);
            }
    
            if (SdirDirSorted != NULL) {
                HeapFree(GetProcessHeap(), 0, SdirDirSorted);
            }
    
            SdirDirCollection = NewSdirDirCollection;
            SdirDirSorted = NewSdirDirSorted;
            SdirDirCollectionCurrent = DirEntsToPreserve;
        }
        
        hFind = FindFirstFile(FindStr, &FindData);
        
        //
        //  If we can't find enumerate, display the error except when we're recursive
        //  and the error is we found no files in this particular directory.
        //

        if (hFind == NULL || hFind == INVALID_HANDLE_VALUE) {

            if (Opts->SubDirWalk == NULL) {
                DWORD Err = GetLastError();
                SdirDisplayError(Err, FindStr);
                SetLastError(Err);
            }
            HeapFree(GetProcessHeap(), 0, FullPath);
            return FALSE;
        }
        
        do {

            _stprintf_s(FullPath, FullPathLength, _T("%s%s"), Opts->ParentName, FindData.cFileName);
            FullPath[FullPathLength - 1] = '\0';

#if defined(UNICODE) && defined(SDIR_SUPPORT_STREAMENUM)
            if (Opts->FindFirstStreamW && (Opts->FtNamedStreams.Flags & SDIR_FEATURE_DISPLAY)) {
                HANDLE hStreamFind;
                SDIR_WIN32_FIND_STREAM_DATA FindStreamData;
                WIN32_FIND_DATA BogusFindData;

                //
                //  Display the default stream
                //

                SdirAddToCollection(&FindData, FullPath);

                //
                //  Look for any named streams
                //

                hStreamFind = Opts->FindFirstStreamW(FullPath, 0, &FindStreamData, 0);
                if (hStreamFind != INVALID_HANDLE_VALUE) {

                    do {

                        //
                        //  For the default stream, just report the information we found
                        //  for the file.  For anything else, query all the equivalent
                        //  stream information
                        //

                        if (_tcscmp(FindStreamData.cStreamName, L"::$DATA") != 0) {

                            DWORD StreamLength = (DWORD)_tcslen(FindStreamData.cStreamName);

                            if (StreamLength > 6 && _tcscmp(FindStreamData.cStreamName + StreamLength - 6, L":$DATA") == 0) {
                                FindStreamData.cStreamName[StreamLength - 6] = '\0';
                            }

                            _stprintf_s(FullPath, FullPathLength, _T("%s%s%s"), Opts->ParentName, FindData.cFileName, FindStreamData.cStreamName);

                            //
                            //  Assume file state is stream state
                            //

                            memcpy(&BogusFindData, &FindData, sizeof(FindData));

                            //
                            //  Populate stream name
                            //

                            _stprintf_s(BogusFindData.cFileName, sizeof(BogusFindData.cFileName)/sizeof(BogusFindData.cFileName[0]), _T("%s%s"), FindData.cFileName, FindStreamData.cStreamName);

                            //
                            //  Populate stream information
                            //

                            SdirUpdateFindDataFromFileInformation(&BogusFindData, FullPath);
                            SdirAddToCollection(&BogusFindData, FullPath);
                        }
                    } while (Opts->FindNextStreamW(hStreamFind, &FindStreamData));
                }

                FindClose(hStreamFind);

            } else {
#endif
                SdirAddToCollection(&FindData, FullPath);
#if defined(UNICODE) && defined(SDIR_SUPPORT_STREAMENUM)
            }
#endif
        } while (FindNextFile(hFind, &FindData) && !Opts->Cancelled);
        
        FindClose(hFind);

        //
        //  If we've encountered more than we've allocated, go back
        //  and reallocate.
        //

    } while (SdirDirCollectionCurrent >= SdirAllocatedDirents);

    HeapFree(GetProcessHeap(), 0, FullPath);
    return TRUE;
}


#define SDIR_LINE_ELEMENT_HORIZ      0
#define SDIR_LINE_ELEMENT_TOP_T      1
#define SDIR_LINE_ELEMENT_BOTTOM_T   2
#define SDIR_LINE_ELEMENT_VERT       3

#ifdef UNICODE
TCHAR SdirLineElementsRich[] = {0x2500, 0x252c, 0x2534, 0x2502};
#endif
TCHAR SdirLineElementsText[] = {'-', '+', '+', '|'};


BOOL
SdirNewlineThroughDisplay()
{
    SDIR_FMTCHAR Line[8];
    DWORD Ext;

    for (Ext = 0; Opts->NewLine[Ext] != '\0'; Ext++) {
        Line[Ext].Char = Opts->NewLine[Ext];
        Line[Ext].Attr = 0;
    }
    SdirWrite(Line, Ext);
    return TRUE;
}

#define SdirFeatureColor(Feat, FileColor) \
    (((Feat)->Flags & SDIR_FEATURE_USE_FILE_COLOR)?FileColor:(Feat)->HighlightColor)


BOOL
SdirDisplayCollection()
{
    PSDIR_DIRENT CurrentEntry;
    DWORD Index, Ext;
    WORD Attributes;
    WORD FeatureColor;
    DWORD Columns;
    DWORD ColumnWidth;
    DWORD ActiveColumn = 0;
    SDIR_FMTCHAR Line[SDIR_MAX_WIDTH];
    DWORD CurrentChar = 0;
    DWORD BufferRows;
    DWORD LongestDisplayedFileName = SdirDirCollectionLongest;
    LPTSTR LineElements = SdirLineElementsText;
    PSDIR_FEATURE Feature;

#ifdef UNICODE
    if (Opts->OutputExtendedCharacters) {
        LineElements = SdirLineElementsRich;
    }
#endif

    //
    //  If we're allowed to shorten names to make the display more
    //  legible, we won't allow a longest name greater than twice
    //  average size.  Since we need to insert three characters to
    //  indicate we butchered a name, don't do this unless we have
    //  a meaningful length to start with (currently 10.)
    //

    if (Opts->EnableNameTruncation) {
        ULONG AverageNameLength;

        AverageNameLength = SdirDirCollectionTotalNameLength / SdirDirCollectionCurrent;
        if (LongestDisplayedFileName > 2 * AverageNameLength) {
            LongestDisplayedFileName = 2 * AverageNameLength;
            if (LongestDisplayedFileName < 10) {
                LongestDisplayedFileName = 10;
            }
        }
    }

    ColumnWidth = Opts->ConsoleWidth;
    if (ColumnWidth > SDIR_MAX_WIDTH) {
        ColumnWidth = SDIR_MAX_WIDTH;
    }

    if (Opts->FtFileName.Flags & SDIR_FEATURE_DISPLAY) {
        Columns = ColumnWidth / (LongestDisplayedFileName + Opts->MetadataWidth);
    } else {
        Columns = ColumnWidth / (Opts->MetadataWidth);
    }

    //_tprintf(_T("MetadataWidth: %i ColumnWidth: %i Columns %i LongestDisplayedFileName %i\n"), Opts->MetadataWidth, ColumnWidth, Columns, LongestDisplayedFileName);

    //
    //  If the output is too big to fit the display, force one column
    //  of sufficient size.  The output here will be scrambled, but it
    //  will indicate to the user what happened.
    //

    if (Columns > 0) {
        ColumnWidth = ColumnWidth / Columns;
        LongestDisplayedFileName = ColumnWidth - Opts->MetadataWidth;
    } else {
        Columns = 1;
        ColumnWidth = Opts->MetadataWidth;
        if (Opts->FtFileName.Flags & SDIR_FEATURE_DISPLAY) {
            ColumnWidth += LongestDisplayedFileName;
        }
    }

    //
    //  This really shouldn't happen, even in the worst case of a MAX_PATH name
    //  with all metadata options enabled, but we'll be paranoid.
    //

    if (Columns * ColumnWidth > SDIR_MAX_WIDTH) {
        SdirWriteString(_T("Too much data for a single line!"));
        return FALSE;
    }

    BufferRows = (SdirDirCollectionCurrent + Columns - 1) / Columns;

    //
    //  Draw the top grid line.
    //

    for (Index = 0; Index < ColumnWidth * Columns; Index++) {
        if (Index % ColumnWidth == ColumnWidth - 1 && Index < (ColumnWidth*Columns-1)) {
            Line[Index].Char = LineElements[SDIR_LINE_ELEMENT_TOP_T];
            Line[Index].Attr = Opts->FtGrid.HighlightColor;
        } else {
            Line[Index].Char = LineElements[SDIR_LINE_ELEMENT_HORIZ];
            Line[Index].Attr = Opts->FtGrid.HighlightColor;
        }
    }

    SdirWrite(Line, Index);

    if (ColumnWidth * Columns != Opts->ConsoleBufferWidth || !Opts->OutputHasAutoLineWrap) {
        SdirNewlineThroughDisplay();
    }

    if (!SdirRowDisplayed()) {
        return FALSE;
    }

    //
    //  Enumerate through the entries.
    //

    for (Index = 0; Index < BufferRows * Columns && !Opts->Cancelled; Index++) {

        //
        //  Because we're sorting down columns first, but rendering a row at a time,
        //  we need to do some matrix math to find which elements belong in which cells.
        //  Some cells in the bottom right might be empty.
        //

        Ext = ActiveColumn * BufferRows + Index / Columns;
        if (Ext < SdirDirCollectionCurrent) {
            CurrentEntry = SdirDirSorted[Ext];
        } else {
            CurrentEntry = NULL;
        }

        //
        //  Render the empty cell.  Or, if we have contents, render that too.
        //

        if (CurrentEntry == NULL) {
            SdirPasteStrAndPad(&Line[CurrentChar], NULL, 0, 0, ColumnWidth - 1);
            CurrentChar += ColumnWidth - 1;
        } else {

            Attributes = CurrentEntry->RenderAttributes;
    
            //
            //  Paste file name into buffer
            //
    
            if (Opts->FtFileName.Flags & SDIR_FEATURE_DISPLAY) {
                FeatureColor = SdirFeatureColor(&Opts->FtFileName, Attributes);
                if (CurrentEntry->FileNameLengthInChars > LongestDisplayedFileName) {
                    ULONG ExtractedLength = (LongestDisplayedFileName - 3) / 2;

                    SdirPasteStr(&Line[CurrentChar], CurrentEntry->FileName, Attributes, ExtractedLength);
                    CurrentChar += ExtractedLength;

                    SdirPasteStr(&Line[CurrentChar], _T("..."), Attributes, ExtractedLength);
                    CurrentChar += 3;

                    SdirPasteStrAndPad(&Line[CurrentChar],
                                       CurrentEntry->FileName + CurrentEntry->FileNameLengthInChars - ExtractedLength,
                                       FeatureColor,
                                       ExtractedLength,
                                       ColumnWidth - Opts->MetadataWidth - ExtractedLength - 3);

                    CurrentChar += ColumnWidth - Opts->MetadataWidth - ExtractedLength - 3;
                } else {
#pragma prefast(suppress: __WARNING_INCORRECT_VALIDATION, File name length is capped and used to calculate column width, which is capped.)
                    SdirPasteStrAndPad(&Line[CurrentChar],
                                       CurrentEntry->FileName,
                                       FeatureColor,
                                       CurrentEntry->FileNameLengthInChars,
                                       ColumnWidth - Opts->MetadataWidth);
                    CurrentChar += ColumnWidth - Opts->MetadataWidth;
                }
            }

            if (Opts->FtShortName.Flags & SDIR_FEATURE_DISPLAY) {
                FeatureColor = SdirFeatureColor(&Opts->FtShortName, Attributes);
                CurrentChar += SdirDisplayShortName(&Line[CurrentChar], FeatureColor, CurrentEntry);

                if (!(Opts->FtFileName.Flags & SDIR_FEATURE_DISPLAY)) {

                    //
                    //  If file name is hidden, we may need to align things, because
                    //  our columns may contain padding (they're essentially justified.)
                    //

#pragma prefast(suppress: __WARNING_INCORRECT_VALIDATION, ColumnWidth is maximum file name size (MAX_PATH) plus metadata, padded up to terminal width, which is capped at the size of this buffer.)
                    SdirPasteStrAndPad(&Line[CurrentChar], NULL, FeatureColor, 0, ColumnWidth - Opts->MetadataWidth);
                    CurrentChar += ColumnWidth - Opts->MetadataWidth;
                }
            }

            //
            //  If file names or short names are being displayed, column
            //  justification has already been performed.  If neither
            //  are displayed, force manual justification here.
            //

            if (!(Opts->FtShortName.Flags & SDIR_FEATURE_DISPLAY || Opts->FtFileName.Flags & SDIR_FEATURE_DISPLAY)) {
                SdirPasteStrAndPad(&Line[CurrentChar],
                                   NULL,
                                   Attributes,
                                   0,
                                   ColumnWidth - Opts->MetadataWidth);
                CurrentChar += ColumnWidth - Opts->MetadataWidth;
            }

            //
            //  Paste any metadata options into the buffer.
            //

            for (Ext = 0; Ext < SdirGetNumSdirExec(); Ext++) {
                Feature = (PSDIR_FEATURE)((PUCHAR)Opts + SdirExec[Ext].FtOffset);
                if (Feature->Flags & SDIR_FEATURE_DISPLAY) {
                    FeatureColor = SdirFeatureColor(Feature, Attributes);
#pragma prefast(suppress: __WARNING_INCORRECT_VALIDATION, All of this adds up to MetadataWidth, which we sanity checked before)
                    CurrentChar += SdirExec[Ext].Function(&Line[CurrentChar], FeatureColor, CurrentEntry);
                }
            }
        }
    
        //
        //  We're starting a new column.  If it's the final one we might want a newline,
        //  otherwise we might want a gridline.  Do this manually so we only write the
        //  line once.
        //

        ActiveColumn++;
        if (ActiveColumn % Columns == 0) {

            for (Ext = 0; Opts->NewLine[Ext] != '\0'; Ext++) {
                Line[CurrentChar].Char = Opts->NewLine[Ext];
                Line[CurrentChar].Attr = SDIR_DEFAULT_COLOR;
                CurrentChar++;
            }
            SdirWrite(Line, CurrentChar);

            CurrentChar = 0;
            ActiveColumn = 0;
            if (!SdirRowDisplayed()) {
                return FALSE;
            }
        } else {
            Line[CurrentChar].Char = LineElements[SDIR_LINE_ELEMENT_VERT];
            Line[CurrentChar].Attr = Opts->FtGrid.HighlightColor;
            CurrentChar++;
        }
    }

    //
    //  Render the bottom gridline.
    //

    for (Index = 0; Index < ColumnWidth * Columns; Index++) {
        if (Index % ColumnWidth == ColumnWidth - 1 && Index < (ColumnWidth*Columns-1)) {
            Line[Index].Char = LineElements[SDIR_LINE_ELEMENT_BOTTOM_T];
            Line[Index].Attr = Opts->FtGrid.HighlightColor;
        } else {
            Line[Index].Char = LineElements[SDIR_LINE_ELEMENT_HORIZ];
            Line[Index].Attr = Opts->FtGrid.HighlightColor;
        }
    }

    SdirWrite(Line, Index);
    CurrentChar = 0;

    if (ColumnWidth * Columns != Opts->ConsoleBufferWidth || !Opts->OutputHasAutoLineWrap) {
        SdirNewlineThroughDisplay();
        CurrentChar = 0;
    }

    if (!SdirRowDisplayed()) {
        return FALSE;
    }

    return TRUE;
}

typedef BOOL (* SDIR_FOR_EACH_PATHSPEC_FN)(LPCTSTR);

BOOL
SdirForEachPathSpec (
    __in int argc,
    __in_ecount(argc) LPTSTR argv[],
    __in SDIR_FOR_EACH_PATHSPEC_FN Callback
    )
{
    LPTSTR FindStr = NULL;
    DWORD FindStrLength = 0;
    LPTSTR EnumPath;
    ULONG  CurrentArg;
    BOOLEAN EnumerateUserSpecified = FALSE;

    for (CurrentArg = 1; CurrentArg < (ULONG)argc; CurrentArg++) {

        if (argv[CurrentArg][0] != '-' && argv[CurrentArg][0] != '/') {

            HANDLE hDir;

            EnumPath = argv[CurrentArg];
            EnumerateUserSpecified = TRUE;

            if (FindStr != NULL) {
                HeapFree(GetProcessHeap(), 0, FindStr);
                FindStr = NULL;
            }

            if (!SdirGetFullPathNameReturnAllocation(EnumPath, &FindStr, NULL)) {
                SdirDisplayError(GetLastError(), _T("SdirGetFullPathName"));
                return FALSE;
            }

            hDir = CreateFile(FindStr,
                              FILE_READ_ATTRIBUTES,
                              FILE_SHARE_READ|FILE_SHARE_WRITE|FILE_SHARE_DELETE,
                              NULL,
                              OPEN_EXISTING,
                              FILE_FLAG_BACKUP_SEMANTICS|FILE_FLAG_OPEN_REPARSE_POINT|FILE_FLAG_OPEN_NO_RECALL,
                              NULL);
    
            if (hDir != INVALID_HANDLE_VALUE) {
                BY_HANDLE_FILE_INFORMATION HandleInfo;
    
                GetFileInformationByHandle(hDir, &HandleInfo);
    
                if (HandleInfo.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
                    LPTSTR FindStrWithWild;
                    LPTSTR szFormatStr;
                    DWORD FindStrWithWildLength = _tcslen(FindStr) + 3;

                    FindStrWithWild = HeapAlloc(GetProcessHeap(), 0, FindStrWithWildLength * sizeof(TCHAR));
                    if (FindStrWithWild == NULL) {
                        SdirDisplayError(GetLastError(), _T("HeapAlloc"));
                        HeapFree(GetProcessHeap(), 0, FindStr);
                        return FALSE;
                    }
                    if (FindStr[FindStrWithWildLength - 4] == '\\') {
                        szFormatStr = _T("%s*");
                    } else {
                        szFormatStr = _T("%s\\*");
                    }
                    _stprintf_s(FindStrWithWild, FindStrWithWildLength, szFormatStr, FindStr);
                    HeapFree(GetProcessHeap(), 0, FindStr);
                    FindStr = FindStrWithWild;
                }
    
                CloseHandle(hDir);
            }

            if (!Callback( FindStr )) {
                HeapFree(GetProcessHeap(), 0, FindStr);
                return FALSE;
            }

        }
    }

    if (FindStr != NULL) {
        HeapFree(GetProcessHeap(), 0, FindStr);
    }

    if (!EnumerateUserSpecified) {
        if (!Callback( _T("*") )) {
            return FALSE;
        }
    }

    return TRUE;
}

BOOL
SdirEnumerateAndDisplay (
    __in int argc,
    __in_ecount(argc) LPTSTR argv[]
    )
{
    if (!SdirForEachPathSpec(argc, argv, SdirEnumeratePath)) {
        return FALSE;
    }

    if (SdirDirCollectionCurrent == 0) {
        SdirDisplayError(ERROR_FILE_NOT_FOUND, NULL);
        return FALSE;
    }

    if (!SdirDisplayCollection()) {
        return FALSE;
    }
    
    return TRUE;
}

ULONG HeirarchyLineNumber = 0;

BOOL
SdirDisplayHeirarchySummary(
    __in LPTSTR NodeName,
    __in PSDIR_SUMMARY Before,
    __in PSDIR_SUMMARY After,
    __in WORD DefaultAttributes
    )
{
    PSDIR_FMTCHAR Buffer;
    TCHAR Str[100];
    DWORD Len;
    DWORD PrefixLen;
    LARGE_INTEGER Size;
    ULONG CurrentChar = 0;
    ULONG MetadataSize = 40;
    ULONG FileCountAlignSize = 4;
    ULONG DirectoryNameAlignSize;
    WORD Background;
    WORD RenderAttributes;

    //
    //  If it doesn't meet size criteria, display nothing
    //

    if (Opts->BriefRecurseSize > 0) {
        if ((After->TotalSize - Before->TotalSize) < Opts->BriefRecurseSize) {
            return TRUE;
        }
    }

    Len = (DWORD)_tcslen(NodeName) + SDIR_MAX_WIDTH;
    Buffer = HeapAlloc(GetProcessHeap(), 0, Len * sizeof(SDIR_FMTCHAR));
    if (Buffer == NULL) {
        SdirDisplayError(GetLastError(), _T("HeapAlloc"));
        return FALSE;
    }

    HeirarchyLineNumber++;

    if ((HeirarchyLineNumber % 2) == 0) {
        Background = Opts->FtBriefAlternate.HighlightColor;
    } else {
        Background = 0;
    }

    //
    //  Guess a good amount of padding based on the window size
    //

    if (Opts->FtCompressedFileSize.Flags & SDIR_FEATURE_DISPLAY) {
        MetadataSize += 20;
    }

    if (Opts->ConsoleWidth < 10 + MetadataSize) {
        DirectoryNameAlignSize = 10;
    } else {
        DirectoryNameAlignSize = Opts->ConsoleWidth - MetadataSize;
    }

    if (DirectoryNameAlignSize > (SDIR_MAX_WIDTH - 100)) {
        DirectoryNameAlignSize = SDIR_MAX_WIDTH - 100;
    }

    if (Opts->ConsoleWidth > 70) {
        FileCountAlignSize++;
        DirectoryNameAlignSize -= 2;
    }

    if (Opts->ConsoleWidth > 100) {
        FileCountAlignSize++;
        DirectoryNameAlignSize -= 2;
    }

    if (Opts->ConsoleWidth > 130) {
        FileCountAlignSize++;
        DirectoryNameAlignSize -= 2;
    }

    //
    //  Display directory name
    //

    RenderAttributes = SdirRenderAttributesFromPath(NodeName);
    Len = (DWORD)_tcslen(NodeName);
    PrefixLen = 0;

    if (SdirIsFullPathUnc(NodeName)) {

        Len -= 8;
        NodeName += 8;

        SdirPasteStr(&Buffer[CurrentChar], _T("\\\\"), (WORD)(RenderAttributes ^ Background), 2);
        CurrentChar += 2;
        PrefixLen = 2;

#if defined(SDIR_SUPPORT_LONGPATH) && defined(UNICODE)
    } else {
        Len -= 4;
        NodeName += 4;
#endif
    }
    
    SdirPasteStr(&Buffer[CurrentChar], NodeName, (WORD)(RenderAttributes ^ Background), Len);
    CurrentChar += Len;
    Len = DirectoryNameAlignSize - ((Len + PrefixLen) % DirectoryNameAlignSize);
    SdirPasteStrAndPad(&Buffer[CurrentChar], NULL, (WORD)(RenderAttributes ^ Background), 0, Len);
    CurrentChar += Len;

    //
    //  Display size of the directory contents
    //

    Size.HighPart = 0;
    SdirFileSizeFromLargeInt(&Size) = After->TotalSize - Before->TotalSize;
    CurrentChar += SdirDisplayGenericSize(&Buffer[CurrentChar], (WORD)(Opts->FtFileSize.HighlightColor ^ Background), &Size);
    SdirPasteStr(&Buffer[CurrentChar], _T(" used "), (WORD)(DefaultAttributes ^ Background), 6);
    CurrentChar += 6;

    //
    //  Display compressed size if requested
    //

    if (Opts->FtCompressedFileSize.Flags & SDIR_FEATURE_DISPLAY) {
        SdirPasteStr(&Buffer[CurrentChar], _T("("), (WORD)(DefaultAttributes ^ Background), 1);
        CurrentChar += 1;

        Size.HighPart = 0;
        SdirFileSizeFromLargeInt(&Size) = After->CompressedSize - Before->CompressedSize;

        CurrentChar += SdirDisplayGenericSize(&Buffer[CurrentChar], (WORD)(Opts->FtCompressedFileSize.HighlightColor ^ Background), &Size);
        SdirPasteStr(&Buffer[CurrentChar], _T(" compressed) "), (WORD)(DefaultAttributes ^ Background), 13);
        CurrentChar += 13;
    }

    //
    //  Display count of files
    //

    _stprintf_s(Str, sizeof(Str)/sizeof(Str[0]), _T("%i"), After->NumFiles - Before->NumFiles);
    Len = (DWORD)_tcslen(Str);

    SdirPasteStrAndPad(&Buffer[CurrentChar], _T("in "), (WORD)(DefaultAttributes ^ Background), 3, 3+FileCountAlignSize);
    CurrentChar += (Len>FileCountAlignSize)?3:(3+FileCountAlignSize-Len);
    SdirPasteStr(&Buffer[CurrentChar], Str, (WORD)(Opts->FtNumberFiles.HighlightColor ^ Background), Len);
    CurrentChar += Len;

    //
    //  And count of dirs
    //

    _stprintf_s(Str, sizeof(Str)/sizeof(Str[0]), _T("%i"), After->NumDirs - Before->NumDirs);
    Len = (DWORD)_tcslen(Str);

    SdirPasteStrAndPad(&Buffer[CurrentChar], _T(" files and "), (WORD)(DefaultAttributes ^ Background), 11, 11+FileCountAlignSize);
    CurrentChar += (Len>FileCountAlignSize)?11:(11+FileCountAlignSize-Len);

    SdirPasteStr(&Buffer[CurrentChar], Str, (WORD)(Opts->FtNumberFiles.HighlightColor ^ Background), Len);
    CurrentChar += Len;

    SdirPasteStr(&Buffer[CurrentChar], _T(" dirs"), (WORD)(DefaultAttributes ^ Background), 5);
    CurrentChar += 5;

    //
    //  Display the formatted line
    //

    for (Len = 0; Len < CurrentChar / Opts->ConsoleWidth; Len++) {
        if (!SdirRowDisplayed()) {
            HeapFree(GetProcessHeap(), 0, Buffer);
            return FALSE;
        }
    }
    SdirWrite(Buffer, CurrentChar);

    //
    //  Newline is written through this function for automatic pause
    //  accounting
    //

    if (!SdirWriteStringWithAttribute(_T("\n"), DefaultAttributes)) {
        HeapFree(GetProcessHeap(), 0, Buffer);
        return FALSE;
    }

    HeapFree(GetProcessHeap(), 0, Buffer);
    return TRUE;
}

#define SdirContinuableError(err) \
    (err == ERROR_FILE_NOT_FOUND || err == ERROR_ACCESS_DENIED)

#define SdirIsReportableError(err) \
    (err != ERROR_FILE_NOT_FOUND)

BOOL
SdirEnumerateAndDisplaySubtree (
    __in TCHAR * TreeRoot,
    __in ULONG Depth,
    __in int argc,
    __in_ecount(argc) LPTSTR argv[]
    )
{
    LPTSTR NextSubDir;
    DWORD NextSubDirLength;
    DWORD TreeRootLen;
    HANDLE hFind;
    WIN32_FIND_DATA FindData;
    ULONG  CurrentArg;
    BOOLEAN EnumerateUserSpecified = FALSE;
    SDIR_SUMMARY SummaryOnEntry;
    LPTSTR szFormatStr;
   
    memcpy(&SummaryOnEntry, Summary, sizeof(SDIR_SUMMARY));

    TreeRootLen = (DWORD)_tcslen(TreeRoot);
    NextSubDirLength = TreeRootLen + SDIR_MAX_FILE_NAME + 2;
    NextSubDir = HeapAlloc(GetProcessHeap(), 0, NextSubDirLength * sizeof(TCHAR));
    if (NextSubDir == NULL) {
        SdirDisplayError(GetLastError(), _T("HeapAlloc"));
        return FALSE;
    }

    //
    //  If the user has specified search criteria, go ahead and collect
    //  those.  There may be multiple.
    //

    for (CurrentArg = 1; CurrentArg < (ULONG)argc; CurrentArg++) {
        if (argv[CurrentArg][0] != '-' && argv[CurrentArg][0] != '/') {

            if (TreeRoot[TreeRootLen - 1] == '\\') {
                szFormatStr = _T("%s%s");
            } else {
                szFormatStr = _T("%s\\%s");
            }

            if (_stprintf_s(NextSubDir, NextSubDirLength, szFormatStr, TreeRoot, argv[CurrentArg]) < 0 ||
                NextSubDir[0] == '\0') {

                SdirWriteString(_T("Path exceeds allocated length\n"));
                HeapFree(GetProcessHeap(), 0, NextSubDir);
                return FALSE;
            }

            EnumerateUserSpecified = TRUE;

            if (!SdirEnumeratePath( NextSubDir )) {
                DWORD Err = GetLastError();
                if (SdirIsReportableError(Err)) {

                    Opts->ErrorsFound = TRUE;
                }
                if (!SdirContinuableError(Err)) {

                    HeapFree(GetProcessHeap(), 0, NextSubDir);
                    return FALSE;
                }
            }
        }
    }

    //
    //  If the user hasn't specified any search criteria, assume "*" and
    //  collect everything.
    //

    if (!EnumerateUserSpecified) {

        if (TreeRoot[TreeRootLen - 1] == '\\') {
            szFormatStr = _T("%s*");
        } else {
            szFormatStr = _T("%s\\*");
        }

        if (_stprintf_s(NextSubDir, NextSubDirLength, szFormatStr, TreeRoot) < 0 ||
            NextSubDir[0] == '\0') {

            SdirWriteString(_T("Path exceeds allocated length\n"));
            HeapFree(GetProcessHeap(), 0, NextSubDir);
            return FALSE;
        }

        if (!SdirEnumeratePath( NextSubDir )) {
            DWORD Err = GetLastError();
            if (SdirIsReportableError(Err)) {

                Opts->ErrorsFound = TRUE;
            }
            if (!SdirContinuableError(Err)) {

                HeapFree(GetProcessHeap(), 0, NextSubDir);
                return FALSE;
            }
        }
    }

    //
    //  If we're giving a regular view and
    //  have something to display, display it.
    //

    if (Opts->BriefRecurseDepth == 0 && SdirDirCollectionCurrent > 0) {

        WORD RenderAttributes;

        RenderAttributes = SdirRenderAttributesFromPath(TreeRoot);

        if (SdirIsFullPathUnc(TreeRoot)) {
            SdirWriteStringWithAttribute(_T("\\\\"), RenderAttributes);
            SdirWriteStringWithAttribute(&TreeRoot[8], RenderAttributes);
        } else {
            SdirWriteStringWithAttribute(&TreeRoot[4], RenderAttributes);
        }
    
        SdirNewlineThroughDisplay();

        if (!SdirRowDisplayed()) {
            HeapFree(GetProcessHeap(), 0, NextSubDir);
            return FALSE;
        }

        if (!SdirDisplayCollection()) {
            HeapFree(GetProcessHeap(), 0, NextSubDir);
            return FALSE;
        }
    }

    //
    //  Now traverse down the tree through all directories,
    //  optionally following links.
    //

    SdirDirCollectionCurrent = 0;
    SdirDirCollectionLongest = 0;
    SdirDirCollectionTotalNameLength = 0;

    if (TreeRoot[TreeRootLen - 1] == '\\') {
        szFormatStr = _T("%s*");
    } else {
        szFormatStr = _T("%s\\*");
    }

    if (_stprintf_s(NextSubDir, NextSubDirLength, szFormatStr, TreeRoot) < 0 ||
        NextSubDir[0] == '\0') {

        SdirWriteString(_T("Path exceeds allocated length\n"));
        HeapFree(GetProcessHeap(), 0, NextSubDir);
        return FALSE;
    }

    hFind = FindFirstFile(NextSubDir, &FindData);
    
    if (hFind == NULL || hFind == INVALID_HANDLE_VALUE) {
        DWORD Err = GetLastError();
        Opts->ErrorsFound = TRUE;
        if (SdirIsReportableError(Err)) {
            if (!SdirDisplayError(Err, NextSubDir)) {
                HeapFree(GetProcessHeap(), 0, NextSubDir);
                return FALSE;
            }
        }
        if (!SdirContinuableError(Err)) {
            HeapFree(GetProcessHeap(), 0, NextSubDir);
            return FALSE;
        } else {
            HeapFree(GetProcessHeap(), 0, NextSubDir);
            return TRUE;
        }
    }
    
    do {
        if (FindData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY &&
            _tcscmp(FindData.cFileName, _T(".")) != 0 &&
            _tcscmp(FindData.cFileName, _T("..")) != 0) {

            if (Opts->TraverseLinks ||
                (FindData.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) == 0 ||
                (FindData.dwReserved0 != IO_REPARSE_TAG_MOUNT_POINT &&
                 FindData.dwReserved0 != IO_REPARSE_TAG_SYMLINK)) {

                if (TreeRoot[TreeRootLen - 1] == '\\') {
                    szFormatStr = _T("%s%s");
                } else {
                    szFormatStr = _T("%s\\%s");
                }

                if (_stprintf_s(NextSubDir, NextSubDirLength, szFormatStr, TreeRoot, FindData.cFileName) < 0 ||
                    NextSubDir[0] == '\0') {

                    SdirWriteString(_T("Path exceeds allocated length\n"));
                    HeapFree(GetProcessHeap(), 0, NextSubDir);
                    return FALSE;
                }
    
                if (!SdirEnumerateAndDisplaySubtree( NextSubDir, Depth + 1, argc, argv )) {
                    FindClose(hFind);
                    HeapFree(GetProcessHeap(), 0, NextSubDir);
                    return FALSE;
                }
            }
        }
    } while (FindNextFile(hFind, &FindData) && !Opts->Cancelled);

    HeapFree(GetProcessHeap(), 0, NextSubDir);
    
    FindClose(hFind);

    //
    //  If we're displaying a heirarchy, display the results at
    //  the end, after all children have been accounted for.
    //

    if (Opts->BriefRecurseDepth != 0 && Depth <= Opts->BriefRecurseDepth) {
        if (!SdirDisplayHeirarchySummary(TreeRoot, &SummaryOnEntry, Summary, Opts->FtSummary.HighlightColor)) {
            return FALSE;
        }
    }

    return TRUE;
}


#ifdef UNICODE
#define ENTRYPOINT wmain
#else
#define ENTRYPOINT main
#endif

int __cdecl
ENTRYPOINT (
    __in int argc,
    __in_ecount(argc) LPTSTR argv[]
    )
{
    if (!SdirInit(argc, argv)) {
        goto restore_and_exit;
    }

    if (Opts->SubDirWalk != NULL) {
#if SDIR_SUPPORT_RECURSIVE
        if (!SdirEnumerateAndDisplaySubtree(Opts->SubDirWalk, 0, argc, argv)) {
            goto restore_and_exit;
        }

        if (Opts->ErrorsFound) {
            SdirWriteStringWithAttribute(_T("Errors found during enumerate; results are incomplete\n"), Opts->FtError.HighlightColor);
        }
#endif
    } else {
        if (!SdirEnumerateAndDisplay(argc, argv)) {
            goto restore_and_exit;
        }
    }

    if (Opts->FtSummary.Flags & SDIR_FEATURE_DISPLAY) {

        SdirDisplaySummary(Opts->FtSummary.HighlightColor);
    }

restore_and_exit:

    SdirSetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), Opts->PreviousAttributes);

    return 0;
}

// vim:sw=4:ts=4:et:
